iT邦幫忙

2024 iThome 鐵人賽

DAY 16
1
Modern Web

Vue 和 TypeScript 的最佳實踐:成為前端工程師的進階利器系列 第 16

Day 16: 如何使用 Pinia 儲存並管理 API 請求的異步數據

  • 分享至 

  • xImage
  •  

https://ithelp.ithome.com.tw/upload/images/20240923/20117461JoY8Bcsghb.jpg

簡介

在現代前端開發中,有效管理 API 請求和異步數據是至關重要的。本文將介紹如何結合 Pinia、@vueuse/core 的 createFetch、Zod 和 TypeScript,創建一個強大而靈活的數據管理系統。我們將在 Day 15 的基礎上,進一步優化我們的用戶管理系統,實現更高效的數據獲取和狀態管理。

實作步驟

步驟 1: 使用 createFetch 重構 API 請求

首先,讓我們使用 @vueuse/core 的 createFetch 來重構我們的 API 請求。這將為我們提供更多的靈活性和功能,如自動取消請求等。

修改 src/composables/useUserApi.ts:

import { createFetch } from '@vueuse/core'
import { CommonHttpStatusCode, userSchema, UserSchema } from "../schemas/user"

const useFetch = createFetch({
  baseUrl: 'http://api.example.com',
  options: {
    async beforeFetch({ options }) {
      // 這裡可以加入認證邏輯
      return { options }
    },
  },
  fetchOptions: {
    mode: 'cors',
  },
})

export const useUserApi = () => {
  const createUserApi = async (user: UserSchema): Promise<UserSchema> => {
    const requestValidator = userSchema.safeParse(user)

    if (!requestValidator.success) {
      console.error(requestValidator.error)
      throw new TypeError('request zod type error')
    }

    const { data, error } = await useFetch('/createUser', {
      method: 'POST',
      body: JSON.stringify(user),
    }).json()

    if (error.value) {
      throw new Error('API request failed')
    }

    const responseValidator = userSchema.safeParse(data.value)
    if (!responseValidator.success) {
      console.error(responseValidator.error)
      throw new TypeError('response zod type error')
    }

    return responseValidator.data
  }

  return {
    createUserApi,
  }
}

export type UseUserApi = typeof useUserApi

備註補充: 以上 beforeFetch 認證邏輯的擴展,基本上大部分的複雜邏輯實現可以參考 Day 13 的文章。
這裡簡單表示,防止文章內文實現過於複雜,關於驗證方面 可以參考 Day 13 的 responseSchema 的驗證,如果是 header 授權相關的,有很多範例,筆者僅展示 Bearer Token 的展示,事實上關於授權相關是個水很深的主題,且嚴謹地遵守 RFC 規範,未來有機會可以寫另外一個 30 天,感謝🙏。

步驟 2: 更新 Pinia Store 以支持取消請求

下一步,我們將更新 Pinia store 以支持取消請求。我們將使用 AbortController 來實現這一功能。

修改 src/stores/useUserStore.ts:

import { acceptHMRUpdate } from 'pinia'
import { definePrivateState } from './privateState'
import { LoadingStatus, UserSchema } from '../schemas/user'
import { useUserApi } from '../composables/useUserApi'
import { useLoadingStore } from './useLoadingStore'
import { ref } from 'vue'

export interface UserStoreState {
  user: UserSchema | null
  error: string | null
}

export const useUserStore = definePrivateState('useUserStore', (): UserStoreState => {
  return {
    user: null,
    error: null
  }
}, privateState => {
  const { createUserApi } = useUserApi()
  const loadingStore = useLoadingStore()
  const { addLoadingStatus, isLoadingStatusExist, removeLoadingStatus, isTypeError } = loadingStore

  const abortController = ref<AbortController | null>(null)

  const createUser = async (user: UserSchema): Promise<boolean> => {
    if (isLoadingStatusExist(LoadingStatus.CreateUser)) return false
    addLoadingStatus(LoadingStatus.CreateUser)

    // 如果有正在進行的請求,先取消它
    if (abortController.value) {
      abortController.value.abort()
    }

    // 創建新的 AbortController
    abortController.value = new AbortController()

    try {
      const createdUser = await createUserApi(user)
      privateState.user = createdUser
      return true
    } catch (error) {
      if (isTypeError(error)) {
        // 如果是型別錯誤在這裡做一些處理
        privateState.error = '資料格式錯誤'
      } else if (error instanceof Error) {
        privateState.error = error.message
      } else {
        privateState.error = '未知錯誤'
      }
      return false
    } finally {
      removeLoadingStatus(LoadingStatus.CreateUser)
      abortController.value = null
    }
  }

  const cancelCurrentRequest = () => {
    if (abortController.value) {
      abortController.value.abort()
      abortController.value = null
      removeLoadingStatus(LoadingStatus.CreateUser)
    }
  }
  
  return {
    createUser,
    cancelCurrentRequest
  }
})

if (import.meta.hot) {
  import.meta.hot.accept(acceptHMRUpdate(useUserStore, import.meta.hot))
}

步驟 3: 更新 Vue 組件以使用新的 Store 功能

現在,讓我們更新我們的 Vue 組件以使用新的 store 功能,包括取消請求的能力。

修改 src/components/UserForm.vue:

<script setup lang="ts">
import CustomInput from '../components/CustomInput.vue'
import { useCreateUserForm } from '../composables/useCreateUserForm'
import { useUserStore } from '../stores/useUserStore'
import { onBeforeUnmount } from 'vue'

const userStore = useUserStore()
const { createUser, cancelCurrentRequest } = userStore

const {
  userName,
  email,
  age,
  formSubmit,
  isSubmittingDisabled,
  errors
} = useCreateUserForm(async (values) => {
  const isApiResponseSuccess = await createUser(values)
  if (isApiResponseSuccess) {
    alert('送出成功')
  }
  return isApiResponseSuccess
})

// 在組件卸載前取消正在進行的請求
onBeforeUnmount(() => {
  cancelCurrentRequest()
})
</script>

<template>
  <!-- 表單內容與之前相同 -->
</template>

步驟 4: 準備 Mock 數據和測試

為了未來能夠輕鬆進行測試,我們可以創建一個 mock 數據文件和一個簡單的 mock API 服務。

創建 src/mocks/userMockData.ts:

import { UserSchema } from '../schemas/user'

export const mockUsers: UserSchema[] = [
  { userName: 'JohnDoe', email: 'john@example.com', age: 30 },
  { userName: 'JaneSmith', email: 'jane@example.com', age: 25 },
]

export const createMockUser = (user: UserSchema): Promise<UserSchema> => {
  return new Promise((resolve) => {
    setTimeout(() => {
      resolve({ ...user, id: Math.random().toString(36).substr(2, 9) })
    }, 1000)
  })
}

步驟 5: 實現 Mock API 切換

為了方便在開發和測試環境中切換between真實 API 和 mock API,我們可以創建一個環境變量和一個工具函數。

創建 src/utils/apiConfig.ts:

export const isUseMockApi = import.meta.env.VITE_USE_MOCK_API === 'true'

export const getApiBaseUrl = () => {
  return isUseMockApi ? '/mock-api' : 'http://api.example.com'
}

然後更新 src/composables/useUserApi.ts:

import { createFetch } from '@vueuse/core'
import { CommonHttpStatusCode, userSchema, UserSchema } from "../schemas/user"
import { getApiBaseUrl, isUseMockApi } from '../utils/apiConfig'
import { createMockUser } from '../mocks/userMockData'

const useFetch = createFetch({
  baseUrl: getApiBaseUrl(),
  options: {
    async beforeFetch({ options }) {
      // 這裡可以加入認證邏輯
      return { options }
    },
  },
  fetchOptions: {
    mode: 'cors',
  },
})

export const useUserApi = () => {
  const createUserApi = async (user: UserSchema): Promise<UserSchema> => {
    const requestValidator = userSchema.safeParse(user)

    if (!requestValidator.success) {
      console.error(requestValidator.error)
      throw new TypeError('request zod type error')
    }

    if (isUseMockApi) {
      return await createMockUser(user)
    }

    const { data, error } = await useFetch('/createUser', {
      method: 'POST',
      body: JSON.stringify(user),
    }).json()

    if (error.value) {
      throw new Error('API request failed')
    }

    const responseValidator = userSchema.safeParse(data.value)
    if (!responseValidator.success) {
      console.error(responseValidator.error)
      throw new TypeError('response zod type error')
    }

    return responseValidator.data
  }

  return {
    createUserApi,
  }
}

export type UseUserApi = typeof useUserApi

結論

在這個 Day 16 的實作中,我們成功地將 Day 15 的基礎上進行了多項改進:

  1. 使用 @vueuse/core 的 createFetch 重構了 API 請求,提供了更多的靈活性和功能。
  2. 在 Pinia store 中實現了請求取消功能,增強了應用的性能和用戶體驗。
  3. 更新了 Vue 組件以利用新的 store 功能,包括在組件卸載時自動取消請求。
  4. 準備了 mock 數據和簡單的 mock API 服務,為未來的測試做好準備。
  5. 實現了在開發和測試環境中輕鬆切換between真實 API 和 mock API 的機制。

這些改進不僅提高了我們應用的可靠性和效能,還為未來的開發和測試奠定了堅實的基礎。通過結合 Pinia、@vueuse/core、Zod 和 TypeScript,我們創建了一個強大而靈活的數據管理系統,能夠有效地處理異步操作和錯誤情況。

在未來的文章中,我們將進一步探討如何利用這個基礎來實現更複雜的功能,如數據緩存、離線支持等高級特性。同時,我們也將深入研究如何編寫單元測試,以確保我們的數據管理邏輯的正確性和穩定性。


上一篇
Day 15: 使用 TypeScript 和 Zod 進行後端 API 數據驗證
下一篇
Day 17: Vee-Validate 和 Zod 結合處理複雜的表單場景 - 進階特性深度探索
系列文
Vue 和 TypeScript 的最佳實踐:成為前端工程師的進階利器30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言